class Crc32 {
    constructor () {
      this.crc = -1
    }
  
    append (data) {
      var crc = this.crc | 0; var table = this.table
      for (var offset = 0, len = data.length | 0; offset < len; offset++) {
        crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
      }
      this.crc = crc
    }
  
    get () {
      return ~this.crc
    }
  }
  Crc32.prototype.table = (() => {
    var i; var j; var t; var table = []
    for (i = 0; i < 256; i++) {
      t = i
      for (j = 0; j < 8; j++) {
        t = (t & 1)
          ? (t >>> 1) ^ 0xEDB88320
          : t >>> 1
      }
      table[i] = t
    }
    return table
  })()
  
  const getDataHelper = byteLength => {
    var uint8 = new Uint8Array(byteLength)
    return {
      array: uint8,
      view: new DataView(uint8.buffer)
    }
  }
  
  const pump = zipObj => zipObj.reader.read().then(chunk => {
    if (chunk.done) return zipObj.writeFooter()
    const outputData = chunk.value
    zipObj.crc.append(outputData)
    zipObj.uncompressedLength += outputData.length
    zipObj.compressedLength += outputData.length
    zipObj.ctrl.enqueue(outputData)
  })
  
  /**
   * [createWriter description]
   * @param  {Object} underlyingSource [description]
   * @return {Boolean}                  [description]
   */
  function createWriter (underlyingSource) {
    const files = Object.create(null)
    const filenames = []
    const encoder = new TextEncoder()
    let offset = 0
    let activeZipIndex = 0
    let ctrl
    let activeZipObject, closed
  
    function next () {
      activeZipIndex++
      activeZipObject = files[filenames[activeZipIndex]]
      if (activeZipObject) processNextChunk()
      else if (closed) closeZip()
    }
  
    var zipWriter = {
      enqueue (fileLike) {
        if (closed) throw new TypeError('Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed')
  
        let name = fileLike.name.trim()
        const date = new Date(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified)
  
        if (fileLike.directory && !name.endsWith('/')) name += '/'
        if (files[name]) throw new Error('File already exists.')
  
        const nameBuf = encoder.encode(name)
        filenames.push(name)
  
        const zipObject = files[name] = {
          level: 0,
          ctrl,
          directory: !!fileLike.directory,
          nameBuf,
          comment: encoder.encode(fileLike.comment || ''),
          compressedLength: 0,
          uncompressedLength: 0,
          writeHeader () {
            var header = getDataHelper(26)
            var data = getDataHelper(30 + nameBuf.length)
  
            zipObject.offset = offset
            zipObject.header = header
            if (zipObject.level !== 0 && !zipObject.directory) {
              header.view.setUint16(4, 0x0800)
            }
            header.view.setUint32(0, 0x14000808)
            header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true)
            header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true)
            header.view.setUint16(22, nameBuf.length, true)
            data.view.setUint32(0, 0x504b0304)
            data.array.set(header.array, 4)
            data.array.set(nameBuf, 30)
            offset += data.array.length
            ctrl.enqueue(data.array)
          },
          writeFooter () {
            var footer = getDataHelper(16)
            footer.view.setUint32(0, 0x504b0708)
  
            if (zipObject.crc) {
              zipObject.header.view.setUint32(10, zipObject.crc.get(), true)
              zipObject.header.view.setUint32(14, zipObject.compressedLength, true)
              zipObject.header.view.setUint32(18, zipObject.uncompressedLength, true)
              footer.view.setUint32(4, zipObject.crc.get(), true)
              footer.view.setUint32(8, zipObject.compressedLength, true)
              footer.view.setUint32(12, zipObject.uncompressedLength, true)
            }
  
            ctrl.enqueue(footer.array)
            offset += zipObject.compressedLength + 16
            next()
          },
          fileLike
        }
  
        if (!activeZipObject) {
          activeZipObject = zipObject
          processNextChunk()
        }
      },
      close () {
        if (closed) throw new TypeError('Cannot close a readable stream that has already been requested to be closed')
        if (!activeZipObject) closeZip()
        closed = true
      }
    }
  
    function closeZip () {
      var length = 0
      var index = 0
      var indexFilename, file
      for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
        file = files[filenames[indexFilename]]
        length += 46 + file.nameBuf.length + file.comment.length
      }
      const data = getDataHelper(length + 22)
      for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
        file = files[filenames[indexFilename]]
        data.view.setUint32(index, 0x504b0102)
        data.view.setUint16(index + 4, 0x1400)
        data.array.set(file.header.array, index + 6)
        data.view.setUint16(index + 32, file.comment.length, true)
        if (file.directory) {
          data.view.setUint8(index + 38, 0x10)
        }
        data.view.setUint32(index + 42, file.offset, true)
        data.array.set(file.nameBuf, index + 46)
        data.array.set(file.comment, index + 46 + file.nameBuf.length)
        index += 46 + file.nameBuf.length + file.comment.length
      }
      data.view.setUint32(index, 0x504b0506)
      data.view.setUint16(index + 8, filenames.length, true)
      data.view.setUint16(index + 10, filenames.length, true)
      data.view.setUint32(index + 12, length, true)
      data.view.setUint32(index + 16, offset, true)
      ctrl.enqueue(data.array)
      ctrl.close()
    }
  
    function processNextChunk () {
      if (!activeZipObject) return
      if (activeZipObject.directory) return activeZipObject.writeFooter(activeZipObject.writeHeader())
      if (activeZipObject.reader) return pump(activeZipObject)
      if (activeZipObject.fileLike.stream) {
        activeZipObject.crc = new Crc32()
        activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
        activeZipObject.writeHeader()
      } else next()
    }
    return new ReadableStream({
      start: c => {
        ctrl = c
        underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter))
      },
      pull () {
        return processNextChunk() || (
          underlyingSource.pull &&
          Promise.resolve(underlyingSource.pull(zipWriter))
        )
      }
    })
  }
  
  window.ZIP = createWriter